package cn.dansj.common.utils.spring.request.servlet;

import cn.dansj.common.utils.io.IOUtils;
import cn.dansj.common.utils.json.JSONUtils;
import cn.dansj.common.utils.json.JSONObject;
import cn.dansj.common.utils.transfer.ArrayUtils;
import cn.dansj.common.utils.transfer.AssertUtils;
import cn.dansj.common.utils.transfer.Transformation;
import cn.dansj.common.utils.transfer.Verification;
import org.springframework.http.MediaType;
import org.springframework.http.server.RequestPath;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.support.MultipartResolutionDelegate;
import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest;
import org.springframework.web.util.ServletRequestPathUtils;
import org.springframework.web.util.UrlPathHelper;

import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.http.*;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

public final class ReadableHttpServletRequest extends ServletRequestWrapper implements HttpServletRequest {
    public static final String PATH_ATTRIBUTE = UrlPathHelper.class.getName() + ".PATH";

    private final byte[] body;

    public ReadableHttpServletRequest(HttpServletRequest httpServletRequest) {
        super(httpServletRequest);
        body = Transformation.lambdaTryCatch(() -> {
            AssertUtils.state("POST".equals(httpServletRequest.getMethod()) && !Transformation.nvl(httpServletRequest.getContentType(), "").contains(MediaType.MULTIPART_FORM_DATA_VALUE), IOException::new);
            return IOUtils.copyToByteArray(httpServletRequest.getInputStream());
        }, () -> "".getBytes(StandardCharsets.UTF_8));
    }

    private HttpServletRequest getHttpServletRequest() {
        return (HttpServletRequest) super.getRequest();
    }

    @Override
    public Object getAttribute(String name) {
        return PATH_ATTRIBUTE.equals(name) ? Transformation.requireNonNullElseGet(super.getAttribute(name), this::getRequestURI) : ServletRequestPathUtils.PATH_ATTRIBUTE.equals(name) ? getRequestPath() : super.getAttribute(name);
    }

    private Object getRequestPath() {
        return Transformation.requireNonNullElseGet(super.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE), () -> ServletRequestPathUtils.parseAndCache(getHttpServletRequest()), () -> RequestPath.parse(getRequestURI(), getContextPath()));
    }

    @Override
    public String getContentType() {
        return Transformation.nvl(super.getContentType(), "");
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        try (final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body)) {
            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return false;
                }

                @Override
                public boolean isReady() {
                    return false;
                }

                @Override
                public void setReadListener(ReadListener readListener) {

                }

                @Override
                public int read() {
                    return byteArrayInputStream.read();
                }
            };
        }
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    public String methodPostParam() {
        final String body = new String(this.body, StandardCharsets.UTF_8);
        if (JSONUtils.isJSON(body) || JSONUtils.isArray(body) || Verification.checkNull(body))
            return body;

        JSONObject params = new JSONObject();
        String[] splitParams = body.split("[&]");
        for (String paramItem : splitParams) {
            String[] item = paramItem.split("[=]", 2);
            if (item.length == 2) {
                String key = item[0];
                String value = item[1];
                params.put(key, value);
            } else {
                params.put(item[0], "");
            }
        }
        return params.toString();
    }

    public String methodGetParam() {
        JSONObject params = new JSONObject();
        final Map<String, String[]> request = getHttpServletRequest().getParameterMap();
        for (String key : request.keySet()) {
            params.put(key, String.join(",", request.get(key)));
        }
        return params.toString();
    }

    public JSONObject getMultipartFile() {
        JSONObject fileMap = JSONObject.of();
        if (MultipartResolutionDelegate.isMultipartRequest(getHttpServletRequest())) {
            final MultipartHttpServletRequest multipartHttpServletRequest = new StandardMultipartHttpServletRequest(getHttpServletRequest());
            final Iterator<String> fileNames = multipartHttpServletRequest.getFileNames();
            while (fileNames.hasNext()) {
                final String f = fileNames.next();
                final MultipartFile file = multipartHttpServletRequest.getFile(f);
                if (Verification.checkNotNull(file)) fileMap.put(f, file.getOriginalFilename());
            }
        }
        return fileMap;
    }

    public String getParam() {
        return getMethod().equals("POST") && !getContentType().startsWith("multipart/form-data;") ? methodPostParam() : methodGetParam();
    }

    /**
     * The default behavior of this method is to return getAuthType() on the wrapped request object.
     */
    @Override
    public String getAuthType() {
        return this.getHttpServletRequest().getAuthType();
    }

    /**
     * The default behavior of this method is to return getCookies() on the wrapped request object.
     */
    @Override
    public Cookie[] getCookies() {
        return this.getHttpServletRequest().getCookies();
    }

    /**
     * The default behavior of this method is to return getDateHeader(String name) on the wrapped request object.
     */
    @Override
    public long getDateHeader(String name) {
        return this.getHttpServletRequest().getDateHeader(name);
    }

    /**
     * The default behavior of this method is to return getHeader(String name) on the wrapped request object.
     */
    @Override
    public String getHeader(String name) {
        return this.getHttpServletRequest().getHeader(name);
    }

    /**
     * The default behavior of this method is to return getHeaders(String name) on the wrapped request object.
     */
    @Override
    public Enumeration<String> getHeaders(String name) {
        return this.getHttpServletRequest().getHeaders(name);
    }

    /**
     * The default behavior of this method is to return getHeaderNames() on the wrapped request object.
     */
    @Override
    public Enumeration<String> getHeaderNames() {
        return this.getHttpServletRequest().getHeaderNames();
    }

    /**
     * The default behavior of this method is to return getIntHeader(String name) on the wrapped request object.
     */
    @Override
    public int getIntHeader(String name) {
        return this.getHttpServletRequest().getIntHeader(name);
    }

    /**
     * The default behavior of this method is to return {@link HttpServletRequest#getHttpServletMapping()} on the
     * wrapped request object.
     *
     * @since Servlet 4.0
     */
    @Override
    public HttpServletMapping getHttpServletMapping() {
        return this.getHttpServletRequest().getHttpServletMapping();
    }

    /**
     * The default behavior of this method is to return getMethod() on the wrapped request object.
     */
    @Override
    public String getMethod() {
        return this.getHttpServletRequest().getMethod();
    }

    /**
     * The default behavior of this method is to return getPathInfo() on the wrapped request object.
     */
    @Override
    public String getPathInfo() {
        return this.getHttpServletRequest().getPathInfo();
    }

    /**
     * The default behavior of this method is to return getPathTranslated() on the wrapped request object.
     */
    @Override
    public String getPathTranslated() {
        return this.getHttpServletRequest().getPathTranslated();
    }

    /**
     * The default behavior of this method is to return getContextPath() on the wrapped request object.
     */
    @Override
    public String getContextPath() {
        return this.getHttpServletRequest().getContextPath();
    }

    /**
     * The default behavior of this method is to return getQueryString() on the wrapped request object.
     */
    @Override
    public String getQueryString() {
        return this.getHttpServletRequest().getQueryString();
    }

    /**
     * The default behavior of this method is to return getRemoteUser() on the wrapped request object.
     */
    @Override
    public String getRemoteUser() {
        return this.getHttpServletRequest().getRemoteUser();
    }

    /**
     * The default behavior of this method is to return isUserInRole(String role) on the wrapped request object.
     */
    @Override
    public boolean isUserInRole(String role) {
        return this.getHttpServletRequest().isUserInRole(role);
    }

    /**
     * The default behavior of this method is to return getUserPrincipal() on the wrapped request object.
     */
    @Override
    public java.security.Principal getUserPrincipal() {
        return this.getHttpServletRequest().getUserPrincipal();
    }

    /**
     * The default behavior of this method is to return getRequestedSessionId() on the wrapped request object.
     */
    @Override
    public String getRequestedSessionId() {
        return this.getHttpServletRequest().getRequestedSessionId();
    }

    /**
     * The default behavior of this method is to return getRequestURI() on the wrapped request object.
     */
    @Override
    public String getRequestURI() {
        return this.getHttpServletRequest().getRequestURI();
    }

    /**
     * The default behavior of this method is to return getRequestURL() on the wrapped request object.
     */
    @Override
    public StringBuffer getRequestURL() {
        final HttpServletRequest httpServletRequest = getHttpServletRequest();
        StringBuffer url = new StringBuffer();
        String scheme = httpServletRequest.getScheme();
        int port = httpServletRequest.getServerPort();
        if (port < 0) port = 80;

        url.append(scheme);
        url.append("://");
        url.append(httpServletRequest.getServerName());
        if (("http".equals(scheme) && (port != 80)) || ("https".equals(scheme) && (port != 443))) {
            url.append(':');
            url.append(port);
        }
        url.append(getRequestURI());

        return url;
    }

    /**
     * The default behavior of this method is to return getServletPath() on the wrapped request object.
     */
    @Override
    public String getServletPath() {
        return this.getHttpServletRequest().getServletPath();
    }

    /**
     * The default behavior of this method is to return getSession(boolean create) on the wrapped request object.
     */
    @Override
    public HttpSession getSession(boolean create) {
        return this.getHttpServletRequest().getSession(create);
    }

    /**
     * The default behavior of this method is to return getSession() on the wrapped request object.
     */
    @Override
    public HttpSession getSession() {
        return this.getHttpServletRequest().getSession();
    }

    /**
     * The default behavior of this method is to call changeSessionId() on the wrapped request object.
     *
     * @since Servlet 3.1
     */
    @Override
    public String changeSessionId() {
        return this.getHttpServletRequest().changeSessionId();
    }

    /**
     * The default behavior of this method is to return isRequestedSessionIdValid() on the wrapped request object.
     */
    @Override
    public boolean isRequestedSessionIdValid() {
        return this.getHttpServletRequest().isRequestedSessionIdValid();
    }

    /**
     * The default behavior of this method is to return isRequestedSessionIdFromCookie() on the wrapped request object.
     */
    @Override
    public boolean isRequestedSessionIdFromCookie() {
        return this.getHttpServletRequest().isRequestedSessionIdFromCookie();
    }

    /**
     * The default behavior of this method is to return isRequestedSessionIdFromURL() on the wrapped request object.
     */
    @Override
    public boolean isRequestedSessionIdFromURL() {
        return this.getHttpServletRequest().isRequestedSessionIdFromURL();
    }

    @Override
    public boolean isRequestedSessionIdFromUrl() {
        return isRequestedSessionIdFromURL();
    }

    /**
     * {@inheritDoc}
     * <p>
     * The default behavior of this method is to return {@link HttpServletRequest#authenticate(HttpServletResponse)} on
     * the wrapped request object.
     *
     * @since Servlet 3.0
     */
    @Override
    public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
        return this.getHttpServletRequest().authenticate(response);
    }

    /**
     * {@inheritDoc}
     * <p>
     * The default behavior of this method is to return {@link HttpServletRequest#login(String, String)} on the wrapped
     * request object.
     *
     * @since Servlet 3.0
     */
    @Override
    public void login(String username, String password) throws ServletException {
        this.getHttpServletRequest().login(username, password);
    }

    /**
     * {@inheritDoc}
     * <p>
     * The default behavior of this method is to return {@link HttpServletRequest#logout()} on the wrapped request
     * object.
     *
     * @since Servlet 3.0
     */
    @Override
    public void logout() throws ServletException {
        this.getHttpServletRequest().logout();
    }

    /**
     * {@inheritDoc}
     * <p>
     * The default behavior of this method is to return {@link HttpServletRequest#getParts()} on the wrapped request
     * object.
     *
     * @since Servlet 3.0
     */
    @Override
    public Collection<Part> getParts() throws IOException, ServletException {
        return this.getHttpServletRequest().getParts();
    }

    /**
     * {@inheritDoc}
     * <p>
     * The default behavior of this method is to return {@link HttpServletRequest#getPart(String)} on the wrapped
     * request object.
     *
     * @since Servlet 3.0
     */
    @Override
    public Part getPart(String name) throws IOException, ServletException {
        return this.getHttpServletRequest().getPart(name);
    }

    /**
     * {@inheritDoc}
     * <p>
     * The default behavior of this method is to return {@link HttpServletRequest#upgrade(Class)} on the wrapped request
     * object.
     *
     * @since Servlet 3.1
     */
    @Override
    public <T extends HttpUpgradeHandler> T upgrade(Class<T> httpUpgradeHandlerClass) throws IOException, ServletException {
        return this.getHttpServletRequest().upgrade(httpUpgradeHandlerClass);
    }

    /**
     * {@inheritDoc}
     * <p>
     * The default behavior of this method is to return {@link HttpServletRequest#newPushBuilder()} on the wrapped
     * request object.
     *
     * @since Servlet 4.0
     */
    @Override
    public PushBuilder newPushBuilder() {
        return this.getHttpServletRequest().newPushBuilder();
    }

    /**
     * {@inheritDoc}
     * <p>
     * The default behavior of this method is to return {@link HttpServletRequest#getTrailerFields()} on the wrapped
     * request object.
     *
     * @since Servlet 4.0
     */
    @Override
    public Map<String, String> getTrailerFields() {
        return this.getHttpServletRequest().getTrailerFields();
    }

    /**
     * {@inheritDoc}
     * <p>
     * The default behavior of this method is to return {@link HttpServletRequest#isTrailerFieldsReady()} on the wrapped
     * request object.
     *
     * @since Servlet 4.0
     */
    @Override
    public boolean isTrailerFieldsReady() {
        return this.getHttpServletRequest().isTrailerFieldsReady();
    }

    @Override
    public String getParameter(String name) {
        return JSONUtils.toJSONObject(getParam()).getString(name);
    }

    @Override
    public Enumeration<String> getParameterNames() {
        return getHttpServletRequest().getParameterNames();
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        final String methodGetParam = methodGetParam();
        final String methodPostParam = methodPostParam();
        final JSONObject methodGet = JSONUtils.isJSON(methodGetParam) ? JSONUtils.toJSONObject(methodGetParam) : JSONObject.of();
        final JSONObject methodPost = JSONUtils.isJSON(methodPostParam) ? JSONUtils.toJSONObject(methodPostParam) : JSONObject.of();
        final Set<String> keySet = ArrayUtils.asSet(methodPost.keySet(), methodGet.keySet());
        Map<String, String[]> parameterMap = new HashMap<>();
        for (String key : keySet) {
            final String methodGetValue = methodGet.getString(key);
            final String methodPostValue = methodPost.getString(key);
            final String[] value = ArrayUtils.asList(methodGetValue, methodPostValue).stream().filter(Verification::checkNotNull).collect(Collectors.toList()).toArray(new String[]{});
            parameterMap.put(key, value);
        }
        return parameterMap;
    }

    @Override
    public String[] getParameterValues(String name) {
        return getParameterMap().get(name);
    }
}